Fix enum const-expression case values, extend pointcut expressiveness, add PHP 8.5 limitations doc - #614
Merged
Conversation
EnumProxyGenerator::resolveEnumData() only recognized String_/Int_ literal
case values; any other constant expression (`case Negative = -1;`,
`case Shifted = 1 << 2;`, `case FromConst = self::SHIFT + 10;`) resolved
to null, so EnumGenerator::addEnumCase() skipped setValue() and the proxy
declared a pure case inside a backed enum — a PHP fatal error ("Case
Negative of backed enum ... must have a value") as soon as the proxy
loaded.
The parser-reflection path now passes the raw PhpParser Expr node through
to EnumGenerator, which emits it verbatim in the proxy enum.
`self::CONST` expressions keep resolving on the proxy because class
constants stay in the woven trait and trait constants participate in the
composing class since PHP 8.2 — verified by a runtime functional test
that loads the woven trait plus proxy enum and asserts the case values
(-1, 4, 12) and from() lookups.
Coverage added:
- EnumGeneratorTest: verbatim emission of Expr case values
- EnumProxyGeneratorTest: native-reflection path emits evaluated scalars
- WeavingTransformerTest: golden woven/proxy fixtures for the const-expr
enum plus a runtime load-and-assert check
- EnumWeavingTest: end-to-end weaving of a project fixture enum with
constant-expression cases
Fixes #600
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
…dicates
ReturnTypePointcut now supports union, intersection and DNF return
types. Both the pattern and the actual reflection type are normalized
into sets of intersection groups (split on '|' at paren depth 0, then
on '&'; parens and leading backslashes removed; a leading '?' on the
actual type expands to '|null'). A single-type pattern matches if any
member of the actual type matches (wildcards preserved per member); a
composite pattern must correspond one-to-one to the actual member set,
order-insensitively. In the pattern, '?' keeps its historical
single-character-wildcard meaning (BC), so nullable patterns are written
as 'Foo|null'. The "not supported yet" note is replaced by the
documented semantics.
The pointcut grammar accepts union/intersection return-type patterns
after ':' (DNF written paren-free, 'A&B|C' == '(A&B)|C'), and new
member modifier predicates 'readonly', 'private(set)' and
'protected(set)', mapped to ReflectionProperty::IS_READONLY /
IS_PRIVATE_SET / IS_PROTECTED_SET. Matching stays bitmask-based in
ModifierPointcut: both native reflection and parser-reflection expose
these bits via getModifiers(), so no implementation-specific guards are
needed. The LALR parse table was regenerated from the updated grammar
(zero conflicts).
The asymmetric-visibility tokens are lexed as single tokens
('private(set)'), keeping full BC for the existing grammar.
Not implemented (out of scope, noted deliberately):
- Parenthesized DNF groups in the grammar ('(A&B)|C'); the paren-free
equivalent parses and matches identically, and ReturnTypePointcut
itself normalizes parenthesized patterns when constructed directly.
- Wildcards inside grammar-level return-type members (direct
ReturnTypePointcut construction supports them).
Fixes #604
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
Add docs/php85-limitations.md, modeled on docs/php84-limitations.md, capturing the PHP 8.5.10 / 8.6.0beta2 audit results from PR #597: Working: pipe operator |> in woven bodies, clone with, #[\NoDiscard] propagation, attributes on class constants (incl. #[\Deprecated]), closures/FCC as parameter defaults, final promoted properties and static asymmetric visibility on non-intercepted properties, and self/parent reflection resolution. Limited (tracked in their issues, several with fixes in flight): closures/FCC in attribute arguments (#601), promoted-property interception (#599), enum const-expression case values (#600), new in initializers under INTERCEPT_INITIALIZATIONS (#603), global constants in attribute args (#602), class-level attributes on woven classes (#598). Static properties (incl. 8.5 static asymmetric visibility) are never interceptable via access() — property hooks do not exist for static properties. README links the new document next to the PHP 8.4 one. Fixes #605 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
…lob() override - Delete 12 fixtures in tests/Instrument/Transformer/_files/ that no test or source file references (verified by grepping each basename across tests/ and src/): anonymous-class(-transformed), file-with-self(-transformed), file-with-self-no-namespace(-transformed), php80-file(-transformed), php81-file(-transformed), php82-file(-transformed). The yii_style.php / yii_style_output.php pair from the original list is NOT deleted — it is still used by FilterInjectorTransformerTest. - Run the PHPStan workflow on a PHP 8.4 + 8.5 matrix with per-version cache keys. - Add parameter and return types to the Symfony\Component\Finder glob() override in tests/functions.php; Finder calls it with a string pattern and an int flag bitmask, so the typed signature stays compatible. Partially addresses #610 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
lisachenko
marked this pull request as ready for review
August 28, 2026 21:41
lisachenko
commented
Aug 29, 2026
The '?' wildcard collides with PHP's nullable-type syntax. NamePointcut and ReturnTypePointcut no longer treat '?' as a one-character wildcard; in return-type patterns a leading '?' is now a real nullable marker, '?Foo' being equivalent to 'Foo|null' on both the pattern and the actual type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
lisachenko
pushed a commit
that referenced
this pull request
Aug 29, 2026
… clean Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four independent changes, one commit each.
1. Enum proxies lose constant-expression case values — Fixes #600
EnumProxyGenerator::resolveEnumData()only recognizedString_/Int_literal case values, socase Negative = -1;,case Shifted = 1 << 2;andcase FromConst = self::SHIFT + 10;resolved tonull,EnumGenerator::addEnumCase()skippedsetValue(), and the proxy declared a pure case inside a backed enum — a PHP fatal error ("Case Negative of backed enum ... must have a value") at proxy load time.Fix: the parser-reflection path now carries the raw PhpParser
Exprnode through toEnumGenerator, which emits it verbatim (Builder\EnumCase::setValue()acceptsExprnatively).self::CONSTkeeps resolving on the proxy because class constants stay in the woven trait and trait constants participate in the composing class since PHP 8.2 — verified at runtime, not just lint: a test weaves the fixture enum, loads the woven trait + proxy enum, and assertsNegative->value === -1,Shifted->value === 4,FromConst->value === 12andfrom(12)works.Tests:
EnumGeneratorTest(verbatimExpremission),EnumProxyGeneratorTest(native-reflection path emits evaluated scalars, never valueless cases),WeavingTransformerTest(goldenphp81-enum-const-expr{,-woven,-proxy}.phpfixtures + runtime load check),EnumWeavingTest(end-to-end project fixtureConstExprBackedEnum).2. Pointcut expressiveness — Fixes #604
(a)
ReturnTypePointcutunion/intersection/DNF support. Pattern and actual type are normalized into sets of intersection groups (split on|at paren depth 0, then&; parens and leading backslashes removed; leading?on the actual type expands to|null). Semantics (documented in the class docblock, "not supported yet" note removed):string,Exc*) matches if any member of the actual type matches (wildcards kept per member);string|int,Countable&Iterator) must correspond one-to-one to the actual member set, order-insensitively;?keeps its historical single-char-wildcard meaning (BC) — nullable patterns are writtenFoo|null.The grammar now accepts union/intersection return types after
:(e.g.execution(public Example->method(*): string|int),...: Countable&Iterator, DNF paren-free asA&B|Cwhich equals(A&B)|Cper PHP precedence).(b) Modifier predicates
readonly,private(set),protected(set)forModifierPointcut/grammar, mapped toReflectionProperty::IS_READONLY/IS_PRIVATE_SET/IS_PROTECTED_SET. Matching stays bitmask-based: both native reflection andGo\ParserReflection\ReflectionPropertyexpose these bits viagetModifiers()(verified against vendor parser-reflection), so nomethod_existsguards are needed. The asymmetric tokens are lexed as single tokens (private(set)), keeping full BC. The LALR parse table was regenerated from the updated grammar — zero conflicts, all pre-existing grammar tests pass unchanged.Deferred (small, deliberate): parenthesized DNF groups in the grammar (
(A&B)|C) — the paren-free equivalent parses and matches identically, andReturnTypePointcutitself normalizes parenthesized patterns when constructed directly; wildcards inside grammar-level return-type members (supported via direct construction). Neither requires further grammar surgery for the issue's use cases.3. docs/php85-limitations.md — Fixes #605
New document modeled on
docs/php84-limitations.md, capturing the PHP 8.5.10 / 8.6.0beta2 audit facts from PR #597: what works (pipe|>in woven bodies,clone with,#[\NoDiscard]propagation with join-point dispatch returning the value, attributes on class constants incl.#[\Deprecated], closures/FCC parameter defaults, final promoted + static asymmetric visibility on non-intercepted properties,self/parentreflection resolution) and what is limited — each phrased as "tracked in #NNN" (#598, #599, #600, #601, #602, #603) since fixes are in flight on other branches — plus the permanent engine constraint that static properties are never interceptable viaaccess()(no property hooks for statics).docs/php84-limitations.mduntouched. README links the new page.4. Hygiene — Partially addresses #610
tests/Instrument/Transformer/_files/after verifying each basename is unreferenced acrosstests/andsrc/. Note:yii_style.php/yii_style_output.phpfrom the issue's list of 14 are not orphaned —FilterInjectorTransformerTeststill reads them — so they were kept..github/workflows/phpstan.yml: PHP 8.4 + 8.5 matrix with per-version cache keys (php-8.4/php-8.5).tests/functions.php: typed the Symfony Finderglob()override asglob(string $pattern, int $flags = 0): array— matching howFinder::searchInDirectory()invokes it (string pattern, intGLOB_*bitmask).Rest of #610 (PARAMETER_WIDENING removal, constructor promotion, replacement coverage fixtures) intentionally not touched — owned by other work.
Test evidence
php8.5 vendor/bin/phpunitphp8.4 vendor/bin/phpunitphp8.5 vendor/bin/phpstan analyze --memory-limit=1Gphp8.6 vendor/bin/phpunit(informational, 8.6.0beta2)Fixes #600
Fixes #604
Fixes #605
Partially addresses #610
Generated by Claude Code